package cscie160.project;

import java.io.Serializable;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.Iterator;
import java.util.Vector;

public class ATMImplementation extends UnicastRemoteObject implements ATM {
    private Vector<ATMListener> registeredListeners;
    private final Bank myBank;
    private final Security mySecurity;
    /** The amount of money in this ATM that is available to be withdrawn. **/
    private float availableFunds;

    public ATMImplementation() throws RemoteException
    {
        registeredListeners = new Vector<ATMListener>();
        myBank = getBankObject();
        mySecurity = getSecurityObject();
        availableFunds = 500;
    }

    /**
     * Deposit an amount of money into our account
     * @param accountInfo The account which is being manipulated, along with security credentials
     * @param amount  The amount to be deposited
     * @throws RemoteException Thrown if performing some unexpected operation
     */
    public void deposit(AccountInfo accountInfo, float amount) throws RemoteException
    {
        notifyListeners(new TransactionNotification("ATM Deposit " + amount + " to account " + accountInfo.getAccountNumber()));
        if (!authenticateAccountInfo(accountInfo))
        {
            throw new RemoteException("Invalid Account Information");
        }
        if (!authorizeAccountInfo(accountInfo.getAccountNumber(), Commands.DEPOSIT))
        {
            throw new RemoteException("Account Not Authorized for Deposit Transaction");
        }
        if (myBank == null)
        {
            throw new RemoteException("Unable to communicate with bank");
        }

        Account depositingAccount = myBank.getAccount(accountInfo.getAccountNumber());
        depositingAccount.deposit(amount);
        System.out.println("New account balance " + depositingAccount.getBalance());
    }
    /**
     * Withdraw an amount of money from our account
     * @param accountInfo The account being manipulated, along with security credentials
     * @param amount  The amount to be withdrawn
     * @throws RemoteException Thrown if performing some unexpected operation
     */
    public void withdraw(AccountInfo accountInfo, float amount) throws RemoteException
    {
        notifyListeners(new TransactionNotification("ATM Withdraw " + amount + " to account " + accountInfo.getAccountNumber()));
        if (!authenticateAccountInfo(accountInfo))
        {
            throw new RemoteException("Invalid Account Information");
        }
        if (!authorizeAccountInfo(accountInfo.getAccountNumber(), Commands.WITHDRAW))
        {
            throw new RemoteException("Account Not Authorized for Withdraw Transaction");
        }
        if (myBank == null)
        {
            throw new RemoteException("Unable to communicate with bank");
        }
        if (amount > availableFunds)
        {
            throw new RemoteException("Amount of requested withdrawal exceeds ATM's current funds.");
        }

        try {
            Account withdrawingAccount = myBank.getAccount(accountInfo.getAccountNumber());
            withdrawingAccount.withdraw(amount);
            System.out.println("New account balance " + withdrawingAccount.getBalance());
            availableFunds -= amount;
        } catch (RemoteException re)
        {
            re.printStackTrace();
            throw new RemoteException("Error executing withdrawal on account.");
        }

    }
    /**
     * Returns the amount of money currently in the account
     * @param accountInfo The account being queried, along with security credentials
     * @return  Account balance
     * @throws RemoteException
     */
    public Float getBalance(AccountInfo accountInfo) throws RemoteException
    {
        //notifyListeners(new TransactionNotification("ATM Balance of " + accountInfo.getAccountNumber()));
        if (!authenticateAccountInfo(accountInfo))
        {
            throw new RemoteException("Invalid Account Information");
        }
        if (!authorizeAccountInfo(accountInfo.getAccountNumber(), Commands.BALANCE))
        {
            throw new RemoteException("Account Not Authorized for Balance Transaction");
        }
        if (myBank == null)
        {
            throw new RemoteException("Unable to communicate with bank");
        }

        Account curAccount = myBank.getAccount(accountInfo.getAccountNumber());
        return curAccount.getBalance();
    }

    /**
     * Transfers an amount of money from one account to another
     * @param fromAccount
     * @param toAccount
     * @param amount
     */
    public void transfer(AccountInfo fromAccount, AccountInfo toAccount, float amount) throws RemoteException
    {
        notifyListeners(new TransactionNotification("ATM Transfer " + amount + " from account " + fromAccount.getAccountNumber() + " to account " + toAccount.getAccountNumber()));
        if (!authenticateAccountInfo(fromAccount))
        {
            throw new RemoteException("Invalid Account Information");
        }
        if (!authorizeAccountInfo(fromAccount.getAccountNumber(), Commands.WITHDRAW))
        {
            throw new RemoteException("Account Not Authorized for Withdraw Transaction");
        }
        if (!authenticateAccountInfo(toAccount))
        {
            throw new RemoteException("Invalid Account Information");
        }
        if (!authorizeAccountInfo(toAccount.getAccountNumber(), Commands.DEPOSIT))
        {
            throw new RemoteException("Account Not Authorized for Withdraw Transaction");
        }
        if (myBank == null)
        {
            throw new RemoteException("Unable to communicate with bank");
        }

        Account withdrawingAccount = myBank.getAccount(fromAccount.getAccountNumber());
        Account depositingAccount = myBank.getAccount(toAccount.getAccountNumber());
        withdrawingAccount.withdraw(amount);
        depositingAccount.deposit(amount);
        System.out.println("New from account balance " + withdrawingAccount.getBalance());
        System.out.println("New to account balance " + depositingAccount.getBalance());
    }

    private void notifyListeners(TransactionNotification notification)
    {
        try
        {
            Iterator<ATMListener> iter = registeredListeners.iterator();
            while (iter.hasNext())
            {
                iter.next().notify(notification);
            }
        } catch (RemoteException re)
        {
            re.printStackTrace();
        }
    }

    private boolean authenticateAccountInfo(AccountInfo accountInfo)
    {
        if (mySecurity == null)
        {
            return false;
        }
        try
        {
            return mySecurity.authenticate(accountInfo);
        } catch (RemoteException re)
        {
            re.printStackTrace();
            return false;
        }
    }

    private boolean authorizeAccountInfo(int accountNumber, Commands command)
    {
        if (mySecurity == null)
        {
            return false;
        }
        try {
            return mySecurity.authorize(accountNumber, command);
        } catch (RemoteException re)
        {
            re.printStackTrace();
            return false;
        }
    }

    private Security getSecurityObject()
    {
        Security returnSecurity = null;
        try {
            System.out.println("Lookup security " + Naming.lookup("//localhost/security"));
            returnSecurity = (Security) Naming.lookup("//localhost/security");
        } catch (RemoteException re)
        {
            System.out.println("An exception occurred while communicating with the security object");
            re.printStackTrace();
        } catch (NotBoundException nbe)
        {
            System.out.println("Could not find the security object");
            nbe.printStackTrace();
        } catch (MalformedURLException mue)
        {
            System.out.println("Malformed URL when looking up the security object");
            mue.printStackTrace();
        }
        return returnSecurity;
    }

    private Bank getBankObject()
    {
        Bank returnBank = null;
        try{
            System.out.println("Lookup bank " + Naming.lookup("//localhost/bank"));
            returnBank = (Bank) Naming.lookup("//localhost/bank");
        } catch (RemoteException re)
        {
            System.out.println("An exception occurred while communicating with the bank object");
            re.printStackTrace();
        } catch (NotBoundException nbe)
        {
            System.out.println("Could not find with the bank object");
            nbe.printStackTrace();
        } catch (MalformedURLException mue)
        {
            System.out.println("Malformed URL when looking up the bank object");
            mue.printStackTrace();
        }
        return returnBank;
    }

    /**
     * Registers a new ATM listener, which will be notified any time transactions involving that listener
     * are conducted.
     * @param listener The listener being registered
     */
    public void addListener(ATMListener listener) throws RemoteException
    {
        registeredListeners.add(listener);
    }

    private void printListeners()
    {
        Iterator<ATMListener> iter = registeredListeners.iterator();
        while (iter.hasNext())
        {
            System.out.println(iter.next());
        }
    }
}
